package core;

import annotation.DependencyInjection;
import annotation.Singleton;
import config.PackageConfig;
import util.ClassUtils;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

/**
 * 单例工厂，先创建所有具有{@link Singleton}注解的类的单例，后可根据类型获取单例
 */
public class SingletonFactory {

    private SingletonFactory() { // 静态工厂类，禁止实例化
        throw new AssertionError();
    }

    private static final Map<Class<?>, Object> singletonMap;

    static {
        singletonMap = new HashMap<>();

        List<Class<?>> classList = new ArrayList<>();
        for (String singletonPackageName : PackageConfig.SINGLETON_PACKAGE_NAMES) {
            classList.addAll(ClassUtils.getClasses(singletonPackageName));
        }

        classList.stream().filter(clazz -> clazz.isAnnotationPresent(Singleton.class)) // 1. filter @Singleton annotation's class
            .forEach(clazz -> { // 2. create Singleton instance and build mapping relationship
                if (singletonMap.containsKey(clazz)) { // Jump out of this loop
                    return;
                }
                Object singleton = constructSingleton(clazz);
                singletonMap.put(clazz, singleton);
            });
    }

    private static Object constructSingleton(Class<?> singletonClass) {
        // 1. get Singleton's @DependencyInjection constructor
        Constructor<?>[] constructors = singletonClass.getConstructors();
        Optional<Constructor<?>> constructor = Arrays.stream(constructors).filter(aConstructor -> aConstructor.isAnnotationPresent(DependencyInjection.class)).findAny();
        if (!constructor.isPresent()) {
            throw new RuntimeException(singletonClass.getName() + " not contains @DependencyInjection constructor");
        }

        // 2. get constructor parameters, every parameter expected to be Singleton too
        Class<?>[] parameterTypes = constructor.get().getParameterTypes();
        Object[] parameterObjects = new Object[parameterTypes.length];
        for (int i = 0; i < parameterTypes.length; i++) {
            Class<?> parameterType = parameterTypes[i];
            // if parameter's Singleton not created before, creating now
            if (!singletonMap.containsKey(parameterType)) {
                Object parameterObject = constructSingleton(parameterType);
                singletonMap.put(parameterType, parameterObject);
            }
            parameterObjects[i] = singletonMap.get(parameterType);
        }

        // 3. construct Singleton
        try {
            return constructor.get().newInstance(parameterObjects);
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    public static <T> T getSingleton(Class<T> singletonClass) {
        if (!singletonMap.containsKey(singletonClass)) {
            throw new RuntimeException(singletonClass.getName() + " cannot be found");
        }
        return singletonClass.cast(singletonMap.get(singletonClass));
    }
}
